Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Rendered β€’ View raw β€’ Download

specs/006-kmp-project-structure/research.md 212bc596e37fd45e75b830e36cd825972f12cf79 (212bc596) Text, 11.59 KB

Research: KMP Recommended Project Structure Alignment

R1: T383838android {} vs T383838androidLibrary {} inside T383838kotlin {}

Decision: Use T383838androidLibrary {} as the canonical DSL accessor for the KMP Android target.

Rationale: The T383838androidLibrary {} accessor is the documented API in the KMP recommended structure guide and the KMP-App-Template. While T383838android {} inside T383838kotlin {} is a transitional alias that still works (used by kotlinconf-app), T383838androidLibrary {} is the forward-looking name that clearly distinguishes between:
β€’ T383838kotlin { androidLibrary {} } β€” KMP module Android target (new plugin)
β€’ T383838android {} β€” top-level Android extension (legacy T383838com.android.library or T383838com.android.application)

Using T383838androidLibrary {} eliminates ambiguity and signals the project uses the recommended patterns.

Alternatives considered:
β€’ Keep T383838android {} inside T383838kotlin {} (works but ambiguous, transitional)
β€’ Use programmatic API only in convention plugins (already done for compileSdk/minSdk; per-module overrides still need DSL)

R2: What properties belong in T383838kotlin.androidLibrary {} vs convention plugin

Decision: Convention plugin (T383838configureKotlinMultiplatform()) handles:
β€’ T383838compileSdk βœ… (already done)
β€’ T383838minSdk βœ… (already done, default from config.properties)
β€’ T383838namespace auto-derivation βœ… (already done when null)
β€’ T383838androidResources.enable = false as default ← NEW (absorb into convention)

Module T383838build.gradle.kts handles only overrides:
β€’ T383838namespace when it differs from auto-derived (e.g., T383838feature.wifiprovision vs T383838feature.wifi-provision)
β€’ T383838minSdk override (only T383838core:proto with minSdk 21)
β€’ T383838androidResources { enable = true; resourcePrefix = "..." } (only T383838core:resources)
β€’ T383838withHostTest {} / T383838withDeviceTest {} (per-module opt-in)

Rationale: The convention plugin already handles compileSdk/minSdk/namespace via T383838KotlinMultiplatformAndroidLibraryTarget. Adding T383838androidResources.enable = false as the convention default eliminates the most common per-module boilerplate (23 of 27 modules set this).

Alternatives considered:
β€’ Move all config to convention plugin, including withHostTest (rejected: test opt-in should be explicit per-module)
β€’ Keep all config in module files (rejected: adds repetitive boilerplate across 27 modules)

R3: Convention plugin audit β€” what needs changing

Decision: The T383838configureKotlinMultiplatform() function in T383838KotlinAndroid.kt is already well-structured. One enhancement needed:

1. Add T383838androidResources.enable = false as default in the T383838pluginManager.withPlugin block alongside compileSdk/minSdk/namespace. This eliminates the most common boilerplate line across 23 modules.

The T383838KmpLibraryConventionPlugin.kt, T383838KmpFeatureConventionPlugin.kt, and T383838KmpJvmAndroidConventionPlugin.kt do NOT need changes β€” they compose correctly and delegate to T383838configureKotlinMultiplatform().

Rationale: The convention plugin chain is already correct:
β€’ T383838KmpLibraryConventionPlugin applies T383838com.android.kotlin.multiplatform.library (correct plugin)
β€’ T383838configureKotlinMultiplatform() uses T383838KotlinMultiplatformAndroidLibraryTarget API (correct API)
β€’ T383838isDesktopOnly guard is properly implemented
β€’ The only gap is that individual modules repeat T383838androidResources.enable = false

Alternatives considered:
β€’ Rewrite convention plugins to use DSL instead of programmatic API (rejected: programmatic API via T383838KotlinMultiplatformAndroidLibraryTarget is cleaner in convention plugins and already works)
β€’ Add T383838withHostTest {} to convention by default (rejected: not all modules need host tests, and some configure T383838isIncludeAndroidResources = true)

R4: Module-by-module android {} block classification

Decision: Categorize all 27 modules into migration tiers based on complexity.

Tier 1: Simple β€” namespace + androidResources.enable = false (6 modules)
These modules only set namespace and disable resources. After convention absorbs T383838androidResources.enable = false, these only need namespace (or nothing if auto-derived matches).

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Module β”‚ namespace β”‚ Auto-derivable? β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838core:di β”‚ T383838org.meshtastic.core.di β”‚ βœ… Yes β”‚
β”‚ T383838core:nfc β”‚ T383838org.meshtastic.core.nfc β”‚ βœ… Yes β”‚
β”‚ T383838core:ui β”‚ T383838org.meshtastic.core.ui β”‚ βœ… Yes β”‚
β”‚ T383838core:navigation β”‚ T383838org.meshtastic.core.navigation β”‚ βœ… Yes β”‚
β”‚ T383838feature:messaging β”‚ T383838org.meshtastic.feature.messaging β”‚ βœ… Yes β”‚
β”‚ T383838feature:settings β”‚ T383838org.meshtastic.feature.settings β”‚ βœ… Yes β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Tier 2: Namespace + resources disabled + withHostTest (18 modules)
These add T383838withHostTest {} opt-in. The T383838androidLibrary {} block will keep withHostTest.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Module β”‚ withHostTest config β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838core:ble β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838core:common β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838core:data β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838core:domain β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838core:model β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838core:network β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838core:service β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838core:takserver β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838feature:connections β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838feature:firmware β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838feature:intro β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838feature:map β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838feature:node β”‚ T383838{ isIncludeAndroidResources = true } β”‚
β”‚ T383838core:datastore β”‚ T383838{} (empty) β”‚
β”‚ T383838core:prefs β”‚ T383838{} (empty) β”‚
β”‚ T383838core:repository β”‚ T383838{} (empty) β”‚
β”‚ T383838core:testing β”‚ T383838{} (empty) β”‚
β”‚ T383838feature:wifi-provision β”‚ T383838{} (empty) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Tier 3: Special cases (3 modules)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Module β”‚ Special config β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838core:proto β”‚ T383838minSdk = 21 override (for ATAK compatibility) β”‚
β”‚ T383838core:database β”‚ T383838namespace + T383838withHostTest + T383838withDeviceTest { instrumentationRunner } β”‚
β”‚ T383838core:resources β”‚ T383838androidResources { enable = true; resourcePrefix = "meshtastic_" } + T383838withHostT… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Not affected (correctly Android-only)
β€’ T383838core:api β€” T383838meshtastic.android.library, AIDL, publishing
β€’ T383838core:barcode β€” T383838meshtastic.android.library, Android-only
β€’ T383838feature:widget β€” T383838meshtastic.android.library, Glance widgets
β€’ T383838app/ β€” T383838com.android.application
β€’ T383838desktop/ β€” JVM only
β€’ T383838screenshot-tests/ β€” Android test module

R5: DESKTOP_ONLY mode compatibility

Decision: No changes needed to DESKTOP_ONLY mode. The migration is purely a DSL rename.

Rationale:
β€’ T383838isDesktopOnly guard in T383838KmpLibraryConventionPlugin skips T383838com.android.kotlin.multiplatform.library plugin application entirely
β€’ T383838configureKotlinMultiplatform() creates inert placeholder T383838androidMain source set in desktop-only mode
β€’ The T383838kotlin { androidLibrary {} } DSL accessor in module build files is provided by the plugin β€” when the plugin isn't applied (desktop-only), the accessor doesn't exist
β€’ The existing T383838android {} accessor inside T383838kotlin {} works the same way β€” so the rename from T383838android {} to T383838androidLibrary {} has identical desktop-only behavior

Key validation: After migration, T383838DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS must succeed.

R6: T383838jvm() declaration in module build files

Decision: Remove redundant T383838jvm() from module build files as a cleanup during migration β€” the convention plugin handles it. Calling T383838jvm() twice is a no-op (Kotlin Gradle Plugin is idempotent for target declarations), so removing it is safe. This reduces boilerplate and makes convention ownership clear.

Alternatives considered: Keep T383838jvm() in all modules for explicitness (rejected: inconsistent since some modules already omit it, and convention plugins are the source of truth).

R7: Namespace auto-derivation coverage

Decision: Rely on convention plugin auto-derivation for most modules; only override where the derived name doesn't match.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Module path β”‚ Auto-derived namespace β”‚ Needed namespace β”‚ Match? β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838:core:ble β”‚ T383838org.meshtastic.core.b… β”‚ T383838org.meshtastic.core.ble β”‚ βœ… β”‚
β”‚ T383838:core:common β”‚ T383838org.meshtastic.core.c… β”‚ T383838org.meshtastic.core.common β”‚ βœ… β”‚
β”‚ T383838:core:data β”‚ T383838org.meshtastic.core.d… β”‚ T383838org.meshtastic.core.data β”‚ βœ… β”‚
β”‚ T383838:core:database β”‚ T383838org.meshtastic.core.d… β”‚ T383838org.meshtastic.core.database β”‚ βœ… β”‚
β”‚ T383838:core:datastore β”‚ T383838org.meshtastic.core.d… β”‚ T383838org.meshtastic.core.datastore β”‚ βœ… β”‚
β”‚ T383838:core:di β”‚ T383838org.meshtastic.core.di β”‚ T383838org.meshtastic.core.di β”‚ βœ… β”‚
β”‚ T383838:core:domain β”‚ T383838org.meshtastic.core.d… β”‚ T383838org.meshtastic.core.domain β”‚ βœ… β”‚
β”‚ T383838:core:model β”‚ T383838org.meshtastic.core.m… β”‚ (none set β€” auto-derived) β”‚ βœ… β”‚
β”‚ T383838:core:navigation β”‚ T383838org.meshtastic.core.n… β”‚ T383838org.meshtastic.core.navigation β”‚ βœ… β”‚
β”‚ T383838:core:network β”‚ T383838org.meshtastic.core.n… β”‚ T383838org.meshtastic.core.network β”‚ βœ… β”‚
β”‚ T383838:core:nfc β”‚ T383838org.meshtastic.core.n… β”‚ T383838org.meshtastic.core.nfc β”‚ βœ… β”‚
β”‚ T383838:core:prefs β”‚ T383838org.meshtastic.core.p… β”‚ T383838org.meshtastic.core.prefs β”‚ βœ… β”‚
β”‚ T383838:core:proto β”‚ T383838org.meshtastic.core.p… β”‚ (none set β€” auto-derived) β”‚ βœ… β”‚
β”‚ T383838:core:repository β”‚ T383838org.meshtastic.core.r… β”‚ (none set β€” auto-derived) β”‚ βœ… β”‚
β”‚ T383838:core:resources β”‚ T383838org.meshtastic.core.r… β”‚ (none set β€” auto-derived) β”‚ βœ… β”‚
β”‚ T383838:core:service β”‚ T383838org.meshtastic.core.s… β”‚ T383838org.meshtastic.core.service β”‚ βœ… β”‚
β”‚ T383838:core:takserver β”‚ T383838org.meshtastic.core.t… β”‚ T383838org.meshtastic.core.takserver β”‚ βœ… β”‚
β”‚ T383838:core:testing β”‚ T383838org.meshtastic.core.t… β”‚ T383838org.meshtastic.core.testing β”‚ βœ… β”‚
β”‚ T383838:core:ui β”‚ T383838org.meshtastic.core.ui β”‚ T383838org.meshtastic.core.ui β”‚ βœ… β”‚
β”‚ T383838:feature:connections β”‚ T383838org.meshtastic.featur… β”‚ T383838org.meshtastic.feature.connections β”‚ βœ… β”‚
β”‚ T383838:feature:firmware β”‚ T383838org.meshtastic.featur… β”‚ T383838org.meshtastic.feature.firmware β”‚ βœ… β”‚
β”‚ T383838:feature:intro β”‚ T383838org.meshtastic.featur… β”‚ T383838org.meshtastic.feature.intro β”‚ βœ… β”‚
β”‚ T383838:feature:map β”‚ T383838org.meshtastic.featur… β”‚ T383838org.meshtastic.feature.map β”‚ βœ… β”‚
β”‚ T383838:feature:messaging β”‚ T383838org.meshtastic.featur… β”‚ T383838org.meshtastic.feature.messaging β”‚ βœ… β”‚
β”‚ T383838:feature:node β”‚ T383838org.meshtastic.featur… β”‚ T383838org.meshtastic.feature.node β”‚ βœ… β”‚
β”‚ T383838:feature:settings β”‚ T383838org.meshtastic.featur… β”‚ T383838org.meshtastic.feature.settings β”‚ βœ… β”‚
β”‚ T383838:feature:wifi-provision β”‚ T383838org.meshtastic.featur… β”‚ T383838org.meshtastic.feature.wifiprovision β”‚ ❌ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Finding: Only T383838feature:wifi-provision has a namespace mismatch β€” the module path contains a hyphen which auto-derives as T383838org.meshtastic.feature.wifi.provision (with dot separator) but the module currently uses T383838org.meshtastic.feature.wifiprovision (no dot). This module MUST explicitly set T383838namespace in its T383838androidLibrary {} block.

All other modules that currently set T383838namespace explicitly match the auto-derived value and can safely rely on the convention plugin's auto-derivation. However, being explicit about namespace provides documentation value and prevents silent changes if the module is moved.

Decision: Remove explicit T383838namespace from modules where it matches auto-derivation. Keep it only for T383838feature:wifi-provision. This reduces boilerplate and delegates ownership to the convention plugin.

Served by rngit 1.5.2 - Generated in 0.09s